<?php
/**
 * Created by PhpStorm.
 * User: jaylen
 * Date: 2020-06-02
 * Time: 16:24
 */

namespace app\common\model;


use app\common\enum\ScopeEnum;
use app\common\exception\TokenException;
use app\common\exception\WxChatException;
use app\common\service\Redis;
use app\common\service\Token;
use think\facade\Cache;
use think\facade\Config;
use app\common\model\WeChatUser as WeChatUserModel;
use think\facade\Log;

class MpApiUserToken extends Token
{
    protected $code;    //用户登录凭证
    protected $wxAppID; //微信小程序appid
    protected $wxAppSecret; //微信小程序appsecret
    protected $wxLoginUrl;  //微信小程序登录凭证校验地址

    protected $redis = null;    //保存redis连接

    const USER_TOKEN_KEY = 'user_uid_token_mp'; //保存用户token和uid关系缓存的键值

    /**
     * 初始化生成UserToken类
     * UserToken constructor.
     * @param $code
     * @throws \think\db\exception\DataNotFoundException
     * @throws \think\db\exception\DbException
     * @throws \think\db\exception\ModelNotFoundException
     */
    public function __construct($code)
    {
        $this->code = $code;
        $this->wxAppID = get_wx_config('mp_app_id');
        $this->wxAppSecret = get_wx_config('mp_app_secret');
        $this->wxLoginUrl = sprintf(Config::get('wx.login_url'),
            $this->wxAppID,$this->wxAppSecret,$this->code);

        // 取出redis配置信息
        $redisConfig = Config::get('redis');
        $this->redis = new Redis($redisConfig);
    }

    /**
     * 根据用户code生成Token
     * @return string
     * @throws TokenException
     * @throws WxChatException
     * @throws \Psr\SimpleCache\InvalidArgumentException
     * @throws \think\db\exception\DataNotFoundException
     * @throws \think\db\exception\DbException
     * @throws \think\db\exception\ModelNotFoundException
     */
    public function getUserToken()
    {
        //得到微信服务器返回的结果
        $wxResult = curl_get($this->wxLoginUrl);
        $wxResult = json_decode($wxResult,true);

        // 判断微信服务器返回的信息是否有误
        if (empty($wxResult)) {
            throw new \Exception('获取session_key及openID时异常，微信内部错误');
        } else {
            // 判断是否有错误码返回
            $loginFail = array_key_exists('errcode',$wxResult);
            if ($loginFail) {
                $this->processWxLoginError($wxResult);
            } else {
                return $this->grantToken($wxResult);
            }
        }
    }

    /**
     * 通过uid更新用户对应Access_token中的值
     * @param $uid 用户id
     * @param $fieldName 需要修改的字段
     * @param $value 对应的值
     * @throws \Psr\SimpleCache\InvalidArgumentException
     * @throws \think\db\exception\DataNotFoundException
     * @throws \think\db\exception\DbException
     * @throws \think\db\exception\ModelNotFoundException
     */
    public function updateUserTokenByUID($uid,$fieldName, $value)
    {
        $accessToken = $this->getUserTokenUIDInRedis($uid);
        if ($accessToken) {
            // 根据access_token获取对应的数据
            $tokenData = Cache::store('redis')->get($accessToken,'');
            if (!empty($tokenData)) {
                if (!is_array($tokenData)) {
                    $tokenData = json_decode($tokenData,true);
                }

                if (isset($tokenData[$fieldName])) {
                    $tokenData[$fieldName] = $value;

                    // 缓存过期时间
                    $expireIn = get_system_config('site_token_expire_in');

                    // 重新写入数据到Access_token中
                    Cache::store('redis')->set($accessToken,$tokenData,$expireIn);
                }

            }
        }

    }

    /**
     * 发布Token令牌
     * @param $wxResult
     * @return string
     * @throws TokenException
     * @throws \Psr\SimpleCache\InvalidArgumentException
     * @throws \think\db\exception\DataNotFoundException
     * @throws \think\db\exception\DbException
     * @throws \think\db\exception\ModelNotFoundException
     */
    protected function grantToken($wxResult)
    {
        //拿到openid
        //到数据库中查看这个openid是否存在了
        //如果存在这不处理，如果不存在则增加一条user记录
        //生成令牌，准备缓存数据，写入缓存
        //把令牌返回到客户端

        $openid = $wxResult['openid'];
        $scope = (get_wx_config('mp_auth_user') == '开启') ? ScopeEnum::GUEST : ScopeEnum::USER;   //是否开启验证功能
        $thirdScope = ScopeEnum::GUEST; // 默认第三方的功能权限为关闭
        $userData = WeChatUserModel::where([['openid','=',$openid]])
            ->field(['id','third_status','status'])
            ->find();
        if (!empty($userData)) {
            $uid = $userData->id;
            if ($userData->status == 1) {
                $scope = ScopeEnum::USER;
            }
            if ($userData->third_status == 1) {
                $thirdScope = ScopeEnum::USER;
            }
        } else {
            $uid = WeChatUserModel::addUserWithOpenid($openid,($scope==ScopeEnum::GUEST) ? 0 : 1);
        }

        // 准备token键对应的值
        $cacheValue = $this->prepareCacheValue($wxResult,$uid,$scope,$thirdScope);
        return $this->saveTokenToCache($cacheValue);
    }

    /**
     * 拼接保存Token对应的值
     * @param $wxResult
     * @param $uid
     * @param int $scope
     * @param int $thirdScope
     * @return mixed
     */
    protected function prepareCacheValue($wxResult,$uid,$scope = ScopeEnum::GUEST,$thirdScope = ScopeEnum::GUEST)
    {
        $cacheValue = $wxResult;
        $cacheValue['uid'] = $uid;
        // 分配权限，默认是游客
        $cacheValue['scope'] = $scope;
        // 分配第三方功能权限
        $cacheValue['third_scope'] = $thirdScope;

        return $cacheValue;
    }

    /**
     * 保存Token到Cache中
     * @param $tokenValue
     * @return string
     * @throws TokenException
     * @throws \Psr\SimpleCache\InvalidArgumentException
     * @throws \think\db\exception\DataNotFoundException
     * @throws \think\db\exception\DbException
     * @throws \think\db\exception\ModelNotFoundException
     */
    protected function saveTokenToCache($tokenValue)
    {
        //生成token作为键
        $key = self::generateToken();
        //将token对应的值转换成json字符串
        $value = json_encode($tokenValue,JSON_UNESCAPED_UNICODE);
        // 缓存过期时间
        $expireIn = get_system_config('token_expire_in');

        $tokenSaveRequest = Cache::store('redis')->set($key,$value,$expireIn);
        //将uid与token值对应关系保存到缓存中，便于及时更新用户认证状态
        $uidTokenSaveRequest = $this->saveUserTokenUIDToRedis($key,$tokenValue['uid']);

        if (!$tokenSaveRequest || !$uidTokenSaveRequest) {
            throw new TokenException([
                'msg' => '服务器缓存异常',
                'errorCode' => 10010,
            ]);
        }

        return $key;
    }

    /**
     * 添加/更新uid与token之间的关系
     * @param $token
     * @param $uid
     * @return bool
     */
    protected function saveUserTokenUIDToRedis($token,$uid)
    {
        // 将uid和token之间的对应关系保存到redis中
        // 如果当前uid作为的键不存在则添加，存在则进行替换
        return $this->redis->setString($this->getTokenUIDRedisKey($uid),$token);
    }

    /**
     * 从Redis中获取对应用户的Access_token的值
     * @param $uid
     * @return bool|string
     */
    protected function getUserTokenUIDInRedis($uid)
    {
        // 获取对应用户的access_token的值
        $value = $this->redis->getString($this->getTokenUIDRedisKey($uid));

        return empty($value) ? false : $value;
    }

    /**
     * 处理微信服务器返回的错误
     * @param $wxResult
     * @throws WxChatException
     */
    protected function processWxLoginError($wxResult)
    {
        Log::record('微信服务器放回错误：'.json_encode($wxResult,JSON_UNESCAPED_UNICODE),'error');
        throw new WxChatException([
            'msg' => $wxResult['errmsg'],
            'errorCode' => $wxResult['errcode'],
        ]);
    }

    /**
     * 获取保存对应用户token和uid之间关系的键
     * @param $uid
     * @return string
     */
    private function getTokenUIDRedisKey($uid)
    {
        return static::USER_TOKEN_KEY . 'uid' . $uid;
    }
}